﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Xml;
using Microsoft.TeamFoundation.WorkItemTracking.Client;

namespace WITD
{
    /// <summary>
    /// 
    /// </summary>
    public enum WITRemovalFromCategoryResult
    {
        /// <summary>
        /// 
        /// </summary>
        Success = 0,
        /// <summary>
        /// 
        /// </summary>
        LastWIT = 1
    }

    /// <summary>
    /// 
    /// </summary>
    class WorkItemCategory
    {
        private Project CurrentProject;

        private const String CategoryTag = "CATEGORY";
        private const String DefaultWITTag = "DEFAULTWORKITEMTYPE";
        private const String WITTag = "WORKITEMTYPE";

        private const String BugCategoryRefName = "Microsoft.BugCategory";
        private const String RequirementCategoryRefName = "Microsoft.RequirementCategory";
        private const String SharedStepCategoryRefName = "Microsoft.SharedStepCategory";
        private const String TestCaseCategoryRefName = "Microsoft.TestCaseCategory";
        
        private const String BugCategoryName = "Bug Category";
        private const String RequirementCategoryName = "Requirement Category";
        private const String SharedStepCategoryName = "Shared Step Category";
        private const String TestCaseCategoryName = "Test Case Category";

        private const String BugCategoryXml = "<CATEGORY refname=\"Microsoft.BugCategory\" name=\"Bug Category\"><DEFAULTWORKITEMTYPE name=\"Bug\" /></CATEGORY>";
        private const String RequirementCategoryXml = "<CATEGORY refname=\"Microsoft.RequirementCategory\" name=\"Requirement Category\"><DEFAULTWORKITEMTYPE name=\"Use Case\" /></CATEGORY>";
        private const String SharedStepCategoryXml = "<CATEGORY refname=\"Microsoft.SharedStepCategory\" name=\"Shared Step Category\"><DEFAULTWORKITEMTYPE name=\"Shared Steps\" /></CATEGORY>";
        private const String TestCaseCategoryXml = "<CATEGORY refname=\"Microsoft.TestCaseCategory\" name=\"Test Case Category\"><DEFAULTWORKITEMTYPE name=\"Test Case\" /></CATEGORY>";

        /// <summary>
        /// Constructor: Specific for a specified project.
        /// </summary>
        /// <param name="project">The project associated with this instance.</param>
        public WorkItemCategory(Project project)
        {
            if (project == null)
                throw new ArgumentNullException("project", "project cannot be null.");

            CurrentProject = project;
        }

        /// <summary>
        /// Retrieves the collection of categories for the specified project.
        /// </summary>
        /// <param name="project"></param>
        /// <returns>A collection of categories associated with the specified project.</returns>
        public CategoryCollection GetCategories(Project project)
        {
            if (project == null)
                throw new ArgumentNullException("project", "project cannot be null.");

            return project.Categories;
        }

        /// <summary>
        /// Imports the categories definition from a source project into a target project.
        /// </summary>
        /// <param name="sourceProject">The source project from which to export categories.</param>
        /// <param name="targetProject">The target project in which to import the categories.</param>
        /// <param name="message">Stores any error messages associated with this method call.</param>
        public void UpdateCategories(Project sourceProject, Project targetProject, out String message)
        {
            message = "";

            if (sourceProject == null)
                throw new ArgumentNullException("sourceProject", "project cannot be null.");

            if (targetProject == null)
                throw new ArgumentNullException("targetProject", "project cannot be null.");
                        
            try
            {
                targetProject.Categories.Import(sourceProject.Categories.Export().DocumentElement);
            }
            catch (Exception ex)
            {
                message = "Error importing categories: " + ex.Message;
            }
        }

        /// <summary>
        /// Gets the first WITD name in the specified category that is not the default WITD.
        /// </summary>
        /// <param name="categoryName">The name of the category to search.</param>
        /// <returns>A non-default WIITD name from the category.</returns>
        public String GetNonDefaultWIT(String categoryName)
        {
            if (String.IsNullOrEmpty(categoryName))
                throw new ArgumentNullException("categoryName", "categoryName cannot be null.");

            String otherWITName = String.Empty;
            WorkItemType defaultWIT = CurrentProject.Categories[categoryName].DefaultWorkItemType;

            foreach (WorkItemType wit in CurrentProject.Categories[categoryName].WorkItemTypes)
            {
                if (wit.Name != defaultWIT.Name)
                {
                    otherWITName = wit.Name;
                }
            }

            return otherWITName;
        }

        /// <summary>
        /// Attempts to create a new category with the specified reference name, name (friendly name), with the specified default work item type.
        /// </summary>
        /// <param name="referenceName">The reference name of the new category.</param>
        /// <param name="friendlyName">The name of the new category.</param>
        /// <param name="defaultWITName">The default work item type of the new category.</param>
        /// <param name="message">Stores any error messages associated with this method call.</param>
        /// <returns>True if successful. Otherwise false.</returns>
        public bool CreateCategory(String referenceName, String friendlyName, String defaultWITName, out String message)
        {
            bool success = true;
            message = "";

            if (String.IsNullOrEmpty(referenceName))
                throw new ArgumentNullException("referenceName", "referenceName cannot be null.");

            if (String.IsNullOrEmpty(friendlyName))
                throw new ArgumentNullException("friendlyName", "friendlyName cannot be null.");

            if (String.IsNullOrEmpty(defaultWITName))
                throw new ArgumentNullException("defaultWITName", "defaultWITName cannot be null.");

            try
            {
                XmlDocument xml = CurrentProject.Categories.Export();

                XmlNode root = xml.ChildNodes[0];

                XmlNode newCatNode = xml.CreateNode(XmlNodeType.Element, "CATEGORY", "");
                
                XmlAttribute refNameAttr = xml.CreateAttribute("refname");
                refNameAttr.Value = referenceName;
                newCatNode.Attributes.Append(refNameAttr);

                XmlAttribute friendlyNameAttr = xml.CreateAttribute("name");
                friendlyNameAttr.Value = friendlyName;
                newCatNode.Attributes.Append(friendlyNameAttr);

                XmlNode defaultWITNode = xml.CreateNode(XmlNodeType.Element, DefaultWITTag, "");

                XmlAttribute defaultWITAttr = xml.CreateAttribute("name");

                defaultWITAttr.Value = defaultWITName;
                defaultWITNode.Attributes.Append(defaultWITAttr);

                newCatNode.AppendChild(defaultWITNode);
                root.AppendChild(newCatNode);

                CurrentProject.Categories.Import(xml.DocumentElement);
                Sync();

            }
            catch (Exception ex)
            {
                success = false;
                message = ex.Message;
            }

            return success;
        }

        /// <summary>
        /// Refreshes and synchronizes the TFS cache using the project passed at instantiation.
        /// </summary>
        private void Sync()
        {
            CurrentProject.Store.RefreshCache();
            CurrentProject.Store.SyncToCache();
        }

        /// <summary>
        /// Refreshes and synchronizes the TFS cache using the specified project.
        /// </summary>
        /// <param name="project">The project to use in order to synchronize to the refreshed cache.</param>
        private void Sync(Project project)
        {
            project.Store.RefreshCache();
            project.Store.SyncToCache();
        }
                        
        /// <summary>
        /// Determines if the specified category contains the specified work item type.
        /// <para></para><para></para>
        /// The specified category is already associated with a project.
        /// </summary>
        /// <param name="cat"></param>
        /// <param name="wit"></param>
        /// <returns></returns>
        private bool CategoryContains(Category cat, WorkItemType wit)
        {
            foreach (WorkItemType wi in cat.WorkItemTypes)
            {
                if (wi.Name == wit.Name)
                {
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// Determines if the named WorkItemType is contained in at least one Category.
        /// </summary>
        /// <param name="project">The Team Project to search.</param>
        /// <param name="wit">The WorkItemType name to search for.</param>
        /// <returns>True if the specified WorkItemType is contained in at least one of the project's categories. Otherwise false;</returns>
        public bool WITInCategory(Project project, WorkItemType wit)
        {
            if (project == null)
                throw new ArgumentNullException("project", "project cannot be null.");

            if (wit == null)
                throw new ArgumentNullException("wit", "wit cannot be null.");

            foreach (Category cat in project.Categories)
            {
                if (CategoryContains(cat, wit))
                {
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// Removes all categories from the specified project. This is to allow work item types to be destroyed more easily because 
        /// if a work item type belongs to a category, it cannot be destroyed.
        /// </summary>
        /// <param name="project">The project from which to delete all categories.</param>
        /// <param name="message">Stores any errors associated with this method call.</param>
        public void DeleteCategories(Project project, out String message)
        {
            message = "";

            if (project == null)
                throw new ArgumentNullException("project", "project cannot be null.");
            try
            {
                XmlDocument categories;
                categories = project.Categories.Export();
                XmlNode root = categories.DocumentElement;

                root.RemoveAll();
                project.Categories.Import(root.OwnerDocument.DocumentElement);
            }
            catch (Exception ex)
            {
                message = ex.Message;
            }
        }

        /// <summary>
        /// Removes the specified category from the project.
        /// </summary>
        /// <param name="project">The project from which to delete the specified category.</param>
        /// <param name="categoryName">The category to delete.</param>
        /// <param name="message">Stores any errors associated with this method call.</param>
        /// <returns></returns>
        public bool RemoveCategory(Project project, String categoryName, out String message)
        {
            XmlDocument categories;
            bool success = true;

            if (project == null)
                throw new ArgumentNullException("project", "project cannot be null.");

            if (String.IsNullOrEmpty(categoryName))
                throw new ArgumentNullException("category", "category cannot be null.");

            message = String.Empty;
            categories = project.Categories.Export();

            foreach (XmlNode node in categories.GetElementsByTagName("CATEGORY"))
            {
                try
                {
                    if (node.Attributes["name"].Value == categoryName)
                    {
                        XmlNode parent = node.ParentNode;
                        parent = parent.RemoveChild(node);
                        break;
                    }
                }
                catch (Exception ex)
                {
                    success = false;
                    message = ex.Message;
                }
            }

            if (success)
            {
                project.Categories.Import(categories.DocumentElement);
                Sync();
            }

            return success; 
        }

        /// <summary>
        /// Removes the specified WorkItemType from the specified Category for the specified project.
        /// </summary>
        /// <param name="project">The project in which the specified category resides.</param>
        /// <param name="categoryName">The category from which to delete the work item type.</param>
        /// <param name="witName">The name of the work item type to remove.</param>
        /// <param name="message">Stores any errors associated with this method call.</param>
        /// <returns>True if successful. Otherwise false.</returns>
        public bool RemoveWITFromCategory(Project project, String categoryName, String witName, out String message)
        {
            XmlDocument categories;
            message = String.Empty;

            if (project == null)
                throw new ArgumentNullException("project", "project cannot be null.");

            if (String.IsNullOrEmpty(categoryName))
                throw new ArgumentNullException("category", "category cannot be null.");

            if (String.IsNullOrEmpty(witName))
                throw new ArgumentNullException("witName", "witName cannot be null.");

            categories = project.Categories.Export();
            bool success = true;
            foreach (XmlNode node in categories.GetElementsByTagName("CATEGORY"))
            {
                if (node.Attributes["name"].Value == categoryName)
                {
                    XmlNodeList witNodes = node.ChildNodes;
                    if (witNodes.Count == 1)
                    {
                        //cannot continue
                        message = "Please add another work item type to this category before trying to remove this one.";
                    }
                    else
                    {
                        try
                        {
                            if (witNodes.Count == 2)
                            {
                                if (witNodes[0].Name == DefaultWITTag)
                                {
                                    if (witNodes[0].Attributes["name"].Value == witName)
                                    {
                                        witNodes[0].Attributes["name"].Value = witNodes[1].Attributes["name"].Value;
                                    }
                                    node.RemoveChild(witNodes[1]);
                                }
                                else
                                {
                                    if (witNodes[0].Attributes["name"].Value != witName)
                                    {
                                        witNodes[1].Attributes["name"].Value = witNodes[0].Attributes["name"].Value;
                                    }
                                    node.RemoveChild(witNodes[0]);
                                }
                            }
                            else
                            {
                                XmlNode defaultNode = null;
                                //more than two nodes
                                foreach (XmlNode witNode in witNodes)
                                {
                                    if (witNode.Attributes["name"].Value == witName)
                                    {
                                        if (witNode.Name == DefaultWITTag)
                                        {
                                            //can't remove this node
                                            defaultNode = witNode;
                                            break;
                                        }
                                        else
                                        {
                                            node.RemoveChild(witNode);
                                            break;
                                        }
                                    }
                                }
                                if (defaultNode != null)
                                {
                                    foreach (XmlNode witNode in witNodes)
                                    {
                                        if (witNode.Name != DefaultWITTag)
                                        {
                                            defaultNode.Attributes["name"].Value = witNode.Attributes["name"].Value;
                                            node.RemoveChild(witNode);
                                            break;
                                        }
                                    }
                                }
                            }
                        }
                        catch (Exception ex)
                        {
                            message = ex.Message;
                            success = false;
                        }
                    }
                }
            }

            if (success)
            {
                project.Categories.Import(categories.DocumentElement);
                Sync();
            }

            return success;
        }

        /// <summary>
        /// Replaces the default work item type with the specified work item type
        /// </summary>
        /// <param name="categoryName">The category in which to replace the default work item type.</param>
        /// <param name="newDefaultWIT">The name of the work item type which will be the new default.</param>
        /// <param name="switchWits">Denotes whether to switch the specified new default work item type with the old default work item type. 
        /// In other words, they simply switch positions in the XML so the old default is still in the category. Otherwise, the old default is removed from 
        /// the category as the specified wit is made the new default.</param>
        /// <param name="message">Stores any errors associated with this method call.</param>
        /// <returns>True if successful. Otherwise false.</returns>
        public bool ReplaceDefaultWITForCategory(String categoryName, String newDefaultWIT, bool switchWits, out String message)
        {
            if (String.IsNullOrEmpty(categoryName))
                throw new ArgumentNullException("category", "category cannot be null.");

            if (String.IsNullOrEmpty(newDefaultWIT))
                throw new ArgumentNullException("newDefaultWIT", "newDefaultWIT cannot be null.");


            XmlElement root = CurrentProject.Categories.Export().DocumentElement;
            XmlNodeList nodes = root.GetElementsByTagName("CATEGORY");
            Category cat = CurrentProject.Categories[categoryName];
            String oldDefaultWitName = String.Empty;
            bool done = false;
            bool success = true;

            message = String.Empty;

            foreach (XmlNode node in nodes)
            {
                if (done) { break; }
                if (node.Attributes["refname"].Value == cat.ReferenceName)
                {
                    try
                    {
                        //found the category catNode
                        XmlNodeList witNodes = node.ChildNodes;
                        foreach (XmlNode witNode in witNodes)
                        {
                            if (witNode.Name == "DEFAULTWORKITEMTYPE")
                            {
                                oldDefaultWitName = witNode.Attributes["name"].Value;
                                witNode.Attributes["name"].Value = newDefaultWIT;
                                break;
                            }
                        }
                        foreach (XmlNode witNode in witNodes)
                        {
                            //find the other catNode and set it to the old default wit -switchWits = true
                            if (witNode.Name == "WORKITEMTYPE" && witNode.Attributes["name"].Value == newDefaultWIT)
                            {
                                if (switchWits)
                                {
                                    witNode.Attributes["name"].Value = oldDefaultWitName;
                                    done = true;
                                    break;
                                }
                                else
                                {
                                    //otherwise remove it altogether
                                    node.RemoveChild(witNode);
                                    break;
                                }
                            }
                        }
                    }
                    catch (Exception ex)
                    {
                        message = ex.Message;
                        success = false;
                        done = true;
                    }
                }
            }
            if (success)
            {
                CurrentProject.Categories.Import(root);
                Sync();
            }

            return success;
        }

        /// <summary>
        /// Attempts to add the specified work item type to the specified category. 
        /// <para></para><para></para>
        /// Attempts to add a work item type to a category that already contains that work item type will fail.
        /// </summary>
        /// <param name="categoryName">The category to which to add the specified work item type.</param>
        /// <param name="witName">The work item type to add to the specified category.</param>
        /// <param name="message">Stores error messages if errors occur.</param>
        /// <returns>True if work item type was added to the category. Otherwise false.</returns>
        public bool AddWITToCategory(String categoryName, String witName, out String message)
        {
            bool success = true;
            message = String.Empty;

            if (String.IsNullOrEmpty(categoryName))
                throw new ArgumentNullException("category", "category cannot be null or empty.");

            if (String.IsNullOrEmpty(witName))
                throw new ArgumentNullException("witName", "witName cannot be null or empty.");

            Category cat = CurrentProject.Categories[categoryName];
            WorkItemType wit = CurrentProject.WorkItemTypes[witName];

            if (CategoryContains(cat, wit)) 
            {
                message = witName + " is already in the " + categoryName + " category.";
                success = false;
            }
            else
            {
                try
                {
                    XmlDocument xml = CurrentProject.Categories.Export();

                    XmlNodeList catNodes = xml.DocumentElement.ChildNodes;
                    foreach (XmlNode catNode in catNodes)
                    {
                        if (catNode.Attributes["name"].Value == categoryName)
                        {
                            //found the category node
                            XmlNode newNode = CreateWITNode(witName, xml);
                            catNode.AppendChild(newNode);
                            CurrentProject.Categories.Import(xml.DocumentElement);
                            break;
                        }
                    }
                }
                catch (Exception ex)
                {
                    message = ex.Message;
                    success = false;
                }
            }
            return success;
            
        }


        /// <summary>
        /// Attempts to add the specified work item type to the specified category. 
        /// <para></para><para></para>
        /// Attempts to add a work item type to a category that already contains that work item type will fail.
        /// </summary>
        /// <param name="categoryName">The category to which to add the specified work item type.</param>
        /// <param name="witNames">The work item types to add to the specified category.</param>
        /// <param name="message">Stores error messages if errors occur.</param>
        /// <returns>True if work item type was added to the category. Otherwise false.</returns>
        public bool AddWITsToCategory(String categoryName, ArrayList witNames, out String message)
        {
            bool success = true;
            bool nodesAdded = false;
            message = String.Empty;

            if (String.IsNullOrEmpty(categoryName))
                throw new ArgumentNullException("category", "category cannot be null or empty.");

            if (witNames == null || witNames.Count == 0)
                throw new ArgumentNullException("witNames", "witNames cannot be null or empty.");

            Category cat = CurrentProject.Categories[categoryName];
            XmlDocument xml = CurrentProject.Categories.Export();
            XmlNodeList catNodes = xml.DocumentElement.ChildNodes;

            foreach (XmlNode catNode in catNodes)
            {
                if (catNode.Attributes["name"].Value == categoryName)
                {
                    //found the category node
                    foreach (String witName in witNames)
                    {
                        WorkItemType wit = CurrentProject.WorkItemTypes[witName];
                        if (CategoryContains(cat, wit))
                        {
                            message += witName + " is already in the " + categoryName + " category." + Environment.NewLine; 
                            success = false;
                        }
                        else
                        {
                            try
                            {
                                XmlNode newNode = CreateWITNode(witName, xml);
                                catNode.AppendChild(newNode);
                                nodesAdded = true;
                            }
                            catch (Exception ex)
                            {
                                message += ex.Message + Environment.NewLine; 
                                success = false;
                            }
                        }
                    }
                    break;
                }
            }
           
            if (nodesAdded)
            {
                CurrentProject.Categories.Import(xml.DocumentElement);
            }

            return success;
        }

        /// <summary>
        /// Creates a new WORKITEMTYPE node with the specified name Attribute and attributeValue.
        /// </summary>
        /// <param name="nodeName">The work item type name to set to the name attribute.</param>
        /// <param name="parentNode">The new node's parent node.</param>
        /// <returns>The newly created node.</returns>
        private XmlNode CreateWITNode(String nodeName, XmlDocument parentNode)
        {
            XmlNode node = null;

            if (parentNode != null)
            {
                node = parentNode.CreateNode(XmlNodeType.Element, "WORKITEMTYPE", "");
                XmlAttribute attr = parentNode.CreateAttribute("name");
                attr.Value = nodeName;
                node.Attributes.Append(attr);
            }
            return node;
        }

        /// <summary>
        /// Determines the number of work item types contained in the specified category.
        /// </summary>
        /// <param name="categoryName">The name of the category to inspect.</param>
        /// <returns>The number of work item types contained in the specified category.</returns>
        public Int32 GetCategoryWITCount(String categoryName)
        {
            if (String.IsNullOrEmpty(categoryName))
                throw new ArgumentNullException("category", "category cannot be null.");
                        
            Category cat = CurrentProject.Categories[categoryName];

            if (cat != null)
            {
                return cat.WorkItemTypes.Count();
            }

            return 0;
        }

        /// <summary>
        /// Gets all work item types in the specified category except the specified work item type to exclude.
        /// </summary>
        /// <param name="categoryName">The category to get work item types for.</param>
        /// <param name="witToExclude">The name of the work item type to exclude from the list.</param>
        /// <exception cref="ArgumentNullException">Thrown if category is null or empty.</exception>
        /// <exception cref="ArgumentNullException">Thrown if witToExclude is null or empty.</exception>
        /// <returns>An array of work item types without the specified work item type that was excluded.</returns>
        public ArrayList GetAvailableWITsForCategory(String categoryName, String witToExclude)
        {
            if (String.IsNullOrEmpty(categoryName))
                throw new ArgumentNullException("category", "category cannot be null.");

            if (String.IsNullOrEmpty(witToExclude))
                throw new ArgumentNullException("witToExclude", "witToExclude cannot be null.");

            IEnumerable<WorkItemType> wits = CurrentProject.Categories[categoryName].WorkItemTypes;
            ArrayList witList = new ArrayList();

            foreach (WorkItemType wit in wits)
            {
                if (wit.Name != witToExclude)
                {
                    witList.Add(wit.Name);
                }
            }

            return witList;
        }

        /// <summary>
        /// Gets the reference name for the specified category name in the current project.
        /// </summary>
        /// <param name="categoryName">The name of the category for which to get the reference name.</param>
        /// <exception cref="ArgumentNullException">Thrown if category is null or empty.</exception>
        /// <exception cref="ArgumentException">Thrown if category is not in the current project.</exception>
        /// <returns>The reference name for the specified category name in the current project. </returns>
        public String GetRefName(String categoryName)
        {
            if (String.IsNullOrEmpty(categoryName))
            {
                throw new ArgumentNullException("categoryName", "categoryName cannot be null or empty.");
            }

            if (!IsValidCategory(categoryName))
                throw new ArgumentException("categoryName", CurrentProject.Name + " does not contain category " + categoryName + ".");
                
            return CurrentProject.Categories[categoryName].ReferenceName;
        }

        /// <summary>
        /// Determines if the specified category exists in the current project.
        /// </summary>
        /// <param name="categoryName">The name of the category to check</param>
        /// <exception cref="ArgumentNullException">Thrown if category is null or empty.</exception>
        /// <returns>True if the current project contains the specified category. Otherwise false.</returns>
        public bool IsValidCategory(String categoryName)
        {
            if (String.IsNullOrEmpty(categoryName))
            {
                throw new ArgumentNullException("categoryName", "categoryName cannot be null or empty.");
            }

            return (CurrentProject.Categories.Contains(categoryName));
        }
    }
}
